﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
using System.Collections.ObjectModel;

namespace Luminal
{
    public enum DeviceType : ulong
    {
        Default = (1 << 0),
        CPU = (1 << 1),
        GPU = (1 << 2),
        Accelerator = (1 << 3),
        All = 0xFFFFFFFF,
    }

    public enum MemoryCacheType : uint
    {
        None = 0x0,
        ReadOnly = 0x1,
        ReadWrite = 0x2,
    }

    public enum LocalMemoryType : uint
    {
        Local = 0x1,
        Global = 0x2,
    }

    public struct FloatingPointConfiguration
    {
        public bool SupportsDenormals;
        public bool SupportsNaN;
        public bool SupportsInfinity;
        public bool SupportsRoundToNearest;
        public bool SupportsRoundToZero;
        public bool SupportsRoundToInfinity;
        public bool SupportsFusedMultiplyAdd;
    }

    public struct ExecutionCapabilities
    {
        public bool SupportsKernelExecution;
        public bool SupportsNativeKernelExecution;
    }

    public struct CommandQueueCapabilities
    {
        public bool SupportsOutOfOrderExecution;
        public bool SupportsProfiling;
    }

    public sealed class DeviceIdentifier : LuminalObject
    {
        #region Interop
        [DllImport("OpenCL.dll")]
        static extern ErrorCode clGetDeviceInfo(IntPtr platform,
            cl_device_info param_name,
            SizeT param_value_size,
            IntPtr param_value,
            out SizeT param_value_size_ret);
        #endregion

        #region Properties
        public DeviceType Type { get; private set; }
        public int VendorID { get; private set; }
        public int ComputeUnits { get; private set; }
        public int[] MaximumWorkItemSizes { get; private set; }
        public int MaximumWorkGroupSize { get; private set; }
        public int PreferredVectorWidthByte { get; private set; }
        public int PreferredVectorWidthShort { get; private set; }
        public int PreferredVectorWidthInteger { get; private set; }
        public int PreferredVectorWidthLong { get; private set; }
        public int PreferredVectorWidthSingle { get; private set; }
        public int PreferredVectorWidthDouble { get; private set; }
        public int MaximumClockFrequency { get; private set; }
        public int AddressBits { get; private set; }
        public int MaximumMemoryAllocationSize { get; private set; }
        public bool SupportsImages { get; private set; }
        public int MaximumImageReadArguments { get; private set; }
        public int MaximumImageWriteArguments { get; private set; }
        public int MaximumImage2DWidth { get; private set; }
        public int MaximumImage2DHeight { get; private set; }
        public int MaximumImage3DWidth { get; private set; }
        public int MaximumImage3DHeight { get; private set; }
        public int MaximumImage3DDepth { get; private set; }
        public int MaximumSamplers { get; private set; }
        public int MaximumParameterSize { get; private set; }
        public int BaseMemoryAddressAlignment { get; private set; }
        public int MinimumDataTypeAlignmentSize { get; private set; }
        public FloatingPointConfiguration FloatingPointConfiguration { get; private set; }
        public MemoryCacheType GlobalMemoryCacheType { get; private set; }
        public int GlobalMemoryCacheLineSize { get; private set; }
        public int GlobalMemoryCacheSize { get; private set; }
        public int GlobalMemorySize { get; private set; }
        public int MaximumConstantBufferSize { get; private set; }
        public int MaximumConstantArguments { get; private set; }
        public LocalMemoryType LocalMemoryType { get; private set; }
        public int LocalMemorySize { get; private set; }
        public bool SupportsErrorCorrection { get; private set; }
        public int ProfilingTimerResolution { get; private set; }
        public bool EndianLittle { get; private set; }
        public bool Available { get { return GetInfo<bool>(cl_device_info.CL_DEVICE_AVAILABLE); } } // This is the one property I would expect to change (lost device?).
        public bool CompilerAvailable { get; private set; }
        public ExecutionCapabilities ExecutionCapabilities { get; private set; }
        public CommandQueueCapabilities CommandQueueCapabilities { get; private set; }
        public PlatformIdentifier Platform { get; private set; }
        public string Name { get; private set; }
        public string Vendor { get; private set; }
        public string DriverVersion { get; private set; }
        public string Profile { get; private set; }
        public string Version { get; private set; }
        public ReadOnlyCollection<string> Extensions { get; private set; }

        private T GetInfo<T>(cl_device_info info) where T : struct
        {
            SizeT infoSize;
            Util.ThrowError(
                "Could not get device property.", 
                clGetDeviceInfo(InternalPointer, info, 0, IntPtr.Zero, out infoSize));

            T output = default(T);
            Util.Pin(ref output, ptr =>
            {
                Util.ThrowError(
                    "Could not get device property.", 
                    clGetDeviceInfo(InternalPointer, info, infoSize, ptr, out infoSize));
            });

            return output;
        }

        private T[] GetInfo<T>(cl_device_info info, int count) where T : struct
        {
            SizeT infoSize;
            T[] output = new T[count];
            Util.Pin(output, ptr =>
            {
                Util.ThrowError(
                    "Could not get device property.", 
                    clGetDeviceInfo(InternalPointer, info, count * Marshal.SizeOf(typeof(T)), ptr, out infoSize));
            });

            return output;
        }

        private string GetInfo(cl_device_info info)
        {
            SizeT infoSize;
            Util.ThrowError(
                "Could not get device property.", 
                clGetDeviceInfo(InternalPointer, info, 0, IntPtr.Zero, out infoSize));

            string output = new string(new char[infoSize - 1]); // minus 1 to account for null terminator
            Util.Pin(ref output, ptr =>
            {
                Util.ThrowError(
                    "Could not get device property.", 
                    clGetDeviceInfo(InternalPointer, info, infoSize, ptr, out infoSize));
            });

            return output;
        }
        #endregion

        #region Constructor
        private DeviceIdentifier(IntPtr ptr)
        {
            Initialize(ptr);

            try
            {
                Type = (DeviceType)GetInfo<uint>(cl_device_info.CL_DEVICE_TYPE);
                VendorID = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_VENDOR_ID);
                ComputeUnits = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_COMPUTE_UNITS);
                MaximumWorkItemSizes = GetInfo<SizeT>(
                    cl_device_info.CL_DEVICE_MAX_WORK_ITEM_SIZES, 
                    (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_WORK_ITEM_DIMENSIONS)).ToIntArray();
                MaximumWorkGroupSize = GetInfo<SizeT>(cl_device_info.CL_DEVICE_MAX_WORK_GROUP_SIZE);
                PreferredVectorWidthByte = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_CHAR);
                PreferredVectorWidthShort = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_SHORT);
                PreferredVectorWidthInteger = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_INT);
                PreferredVectorWidthLong = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_LONG);
                PreferredVectorWidthSingle = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_FLOAT);
                PreferredVectorWidthDouble = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_PREFERRED_VECTOR_WIDTH_DOUBLE);
                MaximumClockFrequency = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_CLOCK_FREQUENCY);
                AddressBits = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_ADDRESS_BITS);
                MaximumMemoryAllocationSize = (int)GetInfo<ulong>(cl_device_info.CL_DEVICE_MAX_MEM_ALLOC_SIZE);
                SupportsImages = GetInfo<bool>(cl_device_info.CL_DEVICE_IMAGE_SUPPORT);
                MaximumImageReadArguments = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_READ_IMAGE_ARGS);
                MaximumImageWriteArguments = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_WRITE_IMAGE_ARGS);
                MaximumImage2DWidth = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_IMAGE2D_MAX_WIDTH);
                MaximumImage2DHeight = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_IMAGE2D_MAX_HEIGHT);
                MaximumImage3DWidth = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_IMAGE3D_MAX_WIDTH);
                MaximumImage3DHeight = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_IMAGE3D_MAX_HEIGHT);
                MaximumImage3DDepth = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_IMAGE3D_MAX_DEPTH);
                MaximumSamplers = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_SAMPLERS);
                MaximumParameterSize = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_PARAMETER_SIZE);
                BaseMemoryAddressAlignment = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MEM_BASE_ADDR_ALIGN);
                MinimumDataTypeAlignmentSize = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MIN_DATA_TYPE_ALIGN_SIZE);

                cl_device_fp_config fpConfig = (cl_device_fp_config)GetInfo<uint>(cl_device_info.CL_DEVICE_SINGLE_FP_CONFIG);
                FloatingPointConfiguration = new FloatingPointConfiguration()
                {
                    SupportsDenormals = (fpConfig & cl_device_fp_config.CL_FP_DENORM) == cl_device_fp_config.CL_FP_DENORM,
                    SupportsFusedMultiplyAdd = (fpConfig & cl_device_fp_config.CL_FP_FMA) == cl_device_fp_config.CL_FP_FMA,
                    SupportsInfinity = (fpConfig & cl_device_fp_config.CL_FP_INF_NAN) == cl_device_fp_config.CL_FP_INF_NAN,
                    SupportsNaN = (fpConfig & cl_device_fp_config.CL_FP_INF_NAN) == cl_device_fp_config.CL_FP_INF_NAN,
                    SupportsRoundToNearest = (fpConfig & cl_device_fp_config.CL_FP_ROUND_TO_NEAREST) == cl_device_fp_config.CL_FP_ROUND_TO_NEAREST,
                    SupportsRoundToInfinity = (fpConfig & cl_device_fp_config.CL_FP_ROUND_TO_INF) == cl_device_fp_config.CL_FP_ROUND_TO_INF,
                    SupportsRoundToZero = (fpConfig & cl_device_fp_config.CL_FP_ROUND_TO_ZERO) == cl_device_fp_config.CL_FP_ROUND_TO_ZERO,
                };

                GlobalMemoryCacheType = (MemoryCacheType)GetInfo<uint>(cl_device_info.CL_DEVICE_GLOBAL_MEM_CACHE_TYPE);
                GlobalMemoryCacheLineSize = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_GLOBAL_MEM_CACHELINE_SIZE);
                GlobalMemoryCacheSize = (int)GetInfo<ulong>(cl_device_info.CL_DEVICE_GLOBAL_MEM_CACHE_SIZE);
                GlobalMemorySize = (int)GetInfo<ulong>(cl_device_info.CL_DEVICE_GLOBAL_MEM_SIZE);
                MaximumConstantBufferSize = (int)GetInfo<ulong>(cl_device_info.CL_DEVICE_MAX_CONSTANT_BUFFER_SIZE);
                MaximumConstantArguments = (int)GetInfo<uint>(cl_device_info.CL_DEVICE_MAX_CONSTANT_ARGS);
                LocalMemoryType = (LocalMemoryType)GetInfo<uint>(cl_device_info.CL_DEVICE_LOCAL_MEM_TYPE);
                LocalMemorySize = (int)GetInfo<ulong>(cl_device_info.CL_DEVICE_LOCAL_MEM_SIZE);
                SupportsErrorCorrection = GetInfo<bool>(cl_device_info.CL_DEVICE_ERROR_CORRECTION_SUPPORT);
                ProfilingTimerResolution = (int)GetInfo<SizeT>(cl_device_info.CL_DEVICE_PROFILING_TIMER_RESOLUTION);
                EndianLittle = GetInfo<bool>(cl_device_info.CL_DEVICE_ENDIAN_LITTLE);
                CompilerAvailable = GetInfo<bool>(cl_device_info.CL_DEVICE_COMPILER_AVAILABLE);

                cl_device_exec_capabilities execCaps = (cl_device_exec_capabilities)GetInfo<ulong>(cl_device_info.CL_DEVICE_EXECUTION_CAPABILITIES);
                ExecutionCapabilities = new ExecutionCapabilities()
                {
                    SupportsKernelExecution = (execCaps & cl_device_exec_capabilities.CL_EXEC_KERNEL) == cl_device_exec_capabilities.CL_EXEC_KERNEL,
                    SupportsNativeKernelExecution = (execCaps & cl_device_exec_capabilities.CL_EXEC_NATIVE_KERNEL) == cl_device_exec_capabilities.CL_EXEC_NATIVE_KERNEL,
                };

                cl_command_queue_properties cqProp = (cl_command_queue_properties)GetInfo<ulong>(cl_device_info.CL_DEVICE_QUEUE_PROPERTIES);
                CommandQueueCapabilities = new CommandQueueCapabilities()
                {
                    SupportsOutOfOrderExecution = (cqProp & cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE) == cl_command_queue_properties.CL_QUEUE_OUT_OF_ORDER_EXEC_MODE_ENABLE,
                    SupportsProfiling = (cqProp & cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE) == cl_command_queue_properties.CL_QUEUE_PROFILING_ENABLE,
                };

                Platform = PlatformIdentifier.FromPointer(GetInfo<IntPtr>(cl_device_info.CL_DEVICE_PLATFORM));
                Name = GetInfo(cl_device_info.CL_DEVICE_NAME);
                Vendor = GetInfo(cl_device_info.CL_DEVICE_VENDOR);
                DriverVersion = GetInfo(cl_device_info.CL_DRIVER_VERSION);
                Profile = GetInfo(cl_device_info.CL_DEVICE_PROFILE);
                Version = GetInfo(cl_device_info.CL_DEVICE_VERSION);
                Extensions = GetInfo(cl_device_info.CL_DEVICE_EXTENSIONS).Trim().Split(' ').
                    Where(n => !string.IsNullOrEmpty(n)).
                    ToList().AsReadOnly();
            }
            catch (Exception ex)
            {
                Dispose();
                throw ex;
            }
        }
        #endregion

        public static DeviceIdentifier FromPointer(IntPtr ptr)
        {
            if (ObjectTracker.ContainsObject(ptr))
                return ObjectTracker.GetObject<DeviceIdentifier>(ptr);
            else
                return new DeviceIdentifier(ptr);
        }
    }
}
